
package apibasej.http;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.Closeable;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.ConnectException;
import java.net.HttpURLConnection;
import java.net.PasswordAuthentication;
import java.net.URL;
import java.net.URLEncoder;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;

import apibasej.basic.exception.APIBaseException;
import apibasej.basic.exception.APIBaseRuntimeException;
import apibasej.basic.misc.io.UtilIO;


/**
 * http://code.google.com/p/apibasej/
 * 
 * @author Julio Azevedo Lessa - apibasej@gmail.com
 * 
 */
public class UtilRequest extends UtilIO implements Closeable{

	
	private TypeReturn typeReturn = TypeReturn.NOT_STARTED;
	
	public enum TypeReturn{
		NOT_STARTED,
		CONNECTING,
		UPLOADING,
		WAITING_FOR_RESPONSE,
		CANCELLED,
		CONNECTION_REFUSED,
		ERROR_CLIENT,
		RESPONSE_READ_ERROR,
		RESPONSE_RECEIVED
	}
	
	private int statusResponseCode = 0;
	private Throwable errorClient;
	
	
	private DataOutputStream out;
	private HttpURLConnection conn;
	
	private int filesProcessed = 0;
	private long totalBytes = 0L;
	private long bytesProcessed = 0L;
	private String currentFileName = "";
	private long begin = 0L;
	private boolean finished = false;
	private String boundary = null;
	private String url = null;
	private final static String NEWLINE = "\r\n";
	private String processMessage = "";
	private boolean abort = false;
	private boolean uploading = false;
	
	// params
	private String cookie;
	private boolean useFullUrl;
	
	
	private ArrayList<String[]> paramsUrl = new ArrayList<String[]>();
	private ArrayList<Field> fields = new ArrayList<Field>();
	private ArrayList<FileUp> files = new ArrayList<FileUp>();
	
	private byte[] dataPost;
	
	private boolean ignoreResponseBody = false;
	//private byte[] responseBody = null;
	
	private String requestMethod = null;
	
	private Map<String,List<String>> responseHeaders = null;
	
	private PasswordAuthentication passwordAuthentication;

	
	private int connectTimeout = 3*60*1000;
	private int readTimeout = 3*60*1000;
	
	
	/**
	 * The boundary prefix.
	 */
	private final String PREFIX = "--";

	public UtilRequest(String url){
		super();
		this.url = url;
		boundary = "--------------------" + Long.toString(System.currentTimeMillis(), 16);
	}
	
	public UtilRequest(){
		super();
		boundary = "--------------------" + Long.toString(System.currentTimeMillis(), 16);
	}

	public long getBytesProcessed() {
		return bytesProcessed;
	}

	public long getTotalBytes() {
		return totalBytes;
	}

	public int getFilesProcessed() {
		return filesProcessed;
	}

	public int getElapsedTime() {

		return (int) (System.currentTimeMillis() - begin);
	}

	public int getTimeLeft() {
		return (int) ((totalBytes - getBytesProcessed()) / (double) getBytesProcessed() * getElapsedTime());
	}

	public int getPercentCompleted() {
		return (int) (getBytesProcessed() / (double) totalBytes * 100);
	}

	public String getCurrentFileName() {
		return currentFileName;
	}

	public String getProcessMessage() {
		return processMessage;
	}



	/**
	 * Interrupts the task.
	 */
	public void stop() {
		closeOut();
		closeConn();
		abort = true;
		setTypeReturn(TypeReturn.CANCELLED);
	}
	
	/**
	 * Returns true if the task is finished.
	 * 
	 * @return True if the task is finished.
	 */
	public boolean finished() {
		return finished;
	}


	public void go() {

		begin = System.currentTimeMillis();
		
		try {
			
			//HttpResponseUtil res = new HttpResponseUtil();
			
			//	sets the default values to 0
			init();
			
			// needed for the progress dialog
			uploading = true;
			
			setTypeReturn(TypeReturn.CONNECTING);
			
			createConn();
			
			setTypeReturn(TypeReturn.UPLOADING);
			
			if(isMultipart()){
				// write the headers to the outputstream
				//writer.write(NEWLINE);
				writeFields();
				writeFiles();
				
				// close
				out.writeBytes(PREFIX);
				out.writeBytes(boundary);
				out.writeBytes(PREFIX);
				out.writeBytes(NEWLINE);
				
			}else if(dataPost!=null && dataPost.length>0){
				
				//out.writeBytes(dataPost); // se fosse string
				
				out.write(dataPost);
				
			}else if(!getFields().isEmpty()){
				
				StringBuilder sb = new StringBuilder();
				boolean appendSep = false;
				for(Field f : getFields()){
					if(appendSep) sb.append('&');
					sb.append(f.getName());
					sb.append("=");
					sb.append(f.getValueEncoded());
					appendSep = true;
				}
				out.writeBytes(sb.toString());
			}
			closeOut();
			
			setTypeReturn(TypeReturn.WAITING_FOR_RESPONSE);
			
			try{
				statusResponseCode = conn.getResponseCode();
				responseHeaders = conn.getHeaderFields();
				
				//if(!ignoreResponseBody){
				//	responseBody = readInputStream(conn.getInputStream());
				//}
				
				setTypeReturn(TypeReturn.RESPONSE_RECEIVED);
				
			}catch (Throwable e) {
				setTypeReturn(TypeReturn.RESPONSE_READ_ERROR);// REVER !!!
				setErrorClient(e);
			}
			
			
		} catch (UnknownHostException uhe) {
			
			if (!abort) {
				setTypeReturn(TypeReturn.ERROR_CLIENT);
				setErrorClient(uhe);
			}
			finished = true;
			
		} catch (ConnectException ce) {
		
			if (!abort) {
				setTypeReturn(TypeReturn.CONNECTION_REFUSED);
				setErrorClient(ce);
			}
			finished = true;
			
		} catch (IOException ioe) {
			
			if (!abort) {
				setTypeReturn(TypeReturn.ERROR_CLIENT);
				setErrorClient(ioe);
			}
			finished = true;
			
		} catch (Throwable t) {

			setTypeReturn(TypeReturn.ERROR_CLIENT);
			setErrorClient(t);
			finished = true;

		} finally {
			closeOut();
			if(ignoreResponseBody)closeConn(); // não fechar para todos, por causa do InputStream
			finished = true;
		}
	}

	private void closeOut(){
		if(out != null){
			try {
				out.flush();
				out.close();
				out = null;
			} catch (Throwable ioe) {
				logDef(Level.SEVERE,"Error on close OutputStream for UtilRequest",ioe);
			}
		}
	}
	private void closeConn(){
		if (conn != null) {
			try {
				conn.disconnect();
				//conn = null; // evitar isto 
			} catch (Throwable ioe) {
				logDef(Level.SEVERE,"Error on disconnect HttpURLConnection for UtilRequest",ioe);
			}
		}
	}
	
	private boolean isSendData(){
		return ((dataPost!=null && dataPost.length>0) || isMultipart() || !getFields().isEmpty());
	}
	
	public HttpURLConnection getHttpURLConnection(){
		return conn;
	}

	private void createConn() throws IOException{
		
		if(this.url==null) throw new APIBaseRuntimeException("URL==null");
		
		URL uploadURL = new URL(getUrlWithParams(this.url));
		this.conn = (HttpURLConnection)uploadURL.openConnection(); // testei rodando local e rodando como applet. Quando roda como applet detecta as configurações do browser !!!! 
        conn.setRequestMethod(getRequestMethod());//"POST");
        conn.setRequestProperty("Accept", "*/*");
        conn.setRequestProperty("User-Agent", getClass().getName());
        conn.setRequestProperty("Connection", "Keep-Alive");
        conn.setRequestProperty("Cache-Control", "no-cache");
        conn.setRequestProperty("Pragma","no-cache");
        
        conn.setConnectTimeout(getConnectTimeout());
        conn.setReadTimeout(getReadTimeout());
        
		if(isMultipart()){
			conn.setRequestProperty("Content-Type","multipart/form-data; boundary=" + boundary);
		}
		if (getCookie() != null && !getCookie().trim().equalsIgnoreCase("")) {
			conn.setRequestProperty("Cookie",getCookie());
		}
		if (getPasswordAuthentication() != null) {// para proxy ...
			// new sun.misc.BASE64Encoder().encode ("dominio\\username:password");
			String loginData = new sun.misc.BASE64Encoder().encode ((getPasswordAuthentication().getUserName() + ":" + new String(getPasswordAuthentication().getPassword())).getBytes());
			conn.setRequestProperty("Proxy-Authorization","Basic "+loginData);
		}
		conn.setRequestProperty("Host",uploadURL.getHost());
		
        //if(!isIgnoreResponseBody())conn.setDoInput(true);// travava esperando resposta ??? acho que não precisa setar isto para ler resposta...
		long contLenSend = 0;
		if(isSendData()) {
			conn.setDoOutput(true);
			if(isMultipart()){
				contLenSend = new LengthCalc().calculateContentLengthMultpart(getFields(), getFilesUp(), boundary.length() + 2);
			}else{
				contLenSend = dataPost.length;
			}
			conn.setRequestProperty("Content-Length", ""+contLenSend );
		}else{
			conn.setDoOutput(false);
		}
		conn.setUseCaches(false);
		conn.setDefaultUseCaches(false);
		
		// se setar para quando não for multipart da problema ...
		if(isMultipart()){
			// quebra em várias requisições ???
			//conn.setChunkedStreamingMode(2*1024);// não usar pois o commons-upload do servidor não funciona com isto, e o apache remove o content-length se usar esta opção
			conn.setFixedLengthStreamingMode((int)contLenSend);// para não fazer buffer antes de enviar
		}
		
		//if(dataPost!=null && dataPost.length()>0) para não dar 405
		if(isSendData()) {
			out = new DataOutputStream(new BufferedOutputStream(conn.getOutputStream()));
		}
	}
	
	

	
	
	// *************************** INI FUNCOES PARA MULTPART ************************************
	private void writeFields() throws IOException {
		for (Field f : getFields()) {
			// write boundary
			out.writeBytes(PREFIX);
			out.writeBytes(boundary);
			out.writeBytes(NEWLINE);

			// write content header
			out.writeBytes("Content-Disposition: form-data; name=\"" + f.getName() + "\"");
			out.writeBytes(NEWLINE);
			out.writeBytes(NEWLINE);
			out.writeBytes(f.getValue());
			out.writeBytes(NEWLINE);
		}
	}
	private void writeFiles() throws IOException {
		for (FileUp f : getFilesUp()) {
			filesProcessed++;
			writeFile(f);			
		}
	}
	private void writeFile(FileUp fu) throws IOException {
		File file = fu.getFile();
		
		// create inputstream
		currentFileName = file.getName();
		BufferedInputStream in = new BufferedInputStream(new FileInputStream(file));

		// write boundary
		out.writeBytes(PREFIX);
		out.writeBytes(boundary);
		out.writeBytes(NEWLINE);

		// write content header
		out.writeBytes("Content-Disposition: form-data; name=\"" + fu.getName() + "\"; filename=\"" + fu.getURLEncodedUploadName() + "\"");
		out.writeBytes(NEWLINE);
		
		if(fu.getContentType()!=null){
			out.writeBytes("Content-Type: " + fu.getContentType());
			out.writeBytes(NEWLINE);
		}
		out.writeBytes(NEWLINE);
		
		// write content
		byte[] data = new byte[1024];
		int len = 0;

		while ((len = in.read(data, 0, data.length)) != -1) {
			out.write(data, 0, len);
			// update properties
			bytesProcessed = bytesProcessed + len;
		}

		out.writeBytes(NEWLINE);
		out.flush();
		in.close();
	}
	// *************************** END FUNCOES PARA MULTPART ************************************
	
	
	
	

	public boolean isUploading() {
		return uploading;
	}
	
	public String getUrl() {
		return url;
	}

	public void setUrl(String url) {
		this.url = url;
	}

	/**
	 * Sets the default values to 0
	 */
	private void init() {
		bytesProcessed = 0;
		totalBytes = getTotalByteSizeOfFiles();
		filesProcessed = 0;		
	}
	
    /**
     * Returns the number of bytes of all files.
     * 
     * @return The number of bytes of all files.
     */
    public long getTotalByteSizeOfFiles() {
        long totalSize = 0L;
        for (FileUp formFile : getFilesUp()) {
                totalSize += formFile.getFile().length();
        }
        return totalSize;
    }

	

	
	/**
	 * This method will be invoked by the FileInfoManager. The method is called
	 * when a new file will be processed. This method is necessary for updating
	 * the progress dialog.
	 * 
	 * @param file The file currently processing.
	 * @param index The index of the file in the sum of all files.
	 * @param size The total of all files.
	 * @return false if the user has aborted.
	 */
	public boolean processingFileInfo(File file, int index, int size) {
		
		filesProcessed = index + 1;
        currentFileName = file.getName();
        bytesProcessed += file.length();
        
        if (abort) {
        	return false;
        } else {
        	return true;
        }
	}

	
	
	
	public Throwable getErrorClient() {
		return errorClient;
	}
	public void setErrorClient(Throwable errorClient) {
		this.errorClient = errorClient;
	}
	public TypeReturn getTypeReturn() {
		return typeReturn;
	}
	public void setTypeReturn(TypeReturn typeReturn) {
		this.typeReturn = typeReturn;
	}
	public void addBytesProcessed (int bytes) {
		bytesProcessed += bytes;
	}
	public String getCookie() {
		return cookie;
	}
	public void setCookie(String cookie) {
		this.cookie = cookie;
	}
	public boolean isUseFullUrl() {
		return useFullUrl;
	}
	public void setUseFullUrl(boolean useFullUrl) {
		this.useFullUrl = useFullUrl;
	}


	

	
	
	public String getUrlWithParams(String url){
		if(getParamsUrl().size()==0) return url;
		boolean primParam = url.indexOf('?')<0;
		for(String[] p : getParamsUrl()){
			if(primParam) url+='?';
			else url+='&';
			primParam = false;
			url += (p[0]+"="+p[1]);
		}
		return url;
	}
	@SuppressWarnings("deprecation")
	public void addParamUrl(String name, String value){// validar tamanho !!!
		try {
			getParamsUrl().add(new String[]{name,URLEncoder.encode(value,getProp(CHARSET))});
		} catch (UnsupportedEncodingException e) {
			getParamsUrl().add(new String[]{name,URLEncoder.encode(value)});
		}
	}
	
	
	
	private ArrayList<String[]> getParamsUrl() {
		if(paramsUrl==null) paramsUrl = new ArrayList<String[]>();
		return paramsUrl;
	}
	public ArrayList<Field> getFields() {
		if(fields==null) fields = new ArrayList<Field>();
		return fields;
	}
	public ArrayList<FileUp> getFilesUp() {
		if(files==null) files = new ArrayList<FileUp>();
		return files;
	}
	public boolean isMultipart() {
		return !getFilesUp().isEmpty();
	}
	public boolean isIgnoreResponseBody() {
		return ignoreResponseBody;
	}
	public void setIgnoreResponseBody(boolean ignoreResponseBody) {
		this.ignoreResponseBody = ignoreResponseBody;
	}
	
	
	public int getResponseContentLength(){
		return conn.getContentLength();
	}
	
	// lança exceção se não tiver na casa dos 200 // java.io.IOException: Server returned HTTP response code: 500 for URL: http://localhost:8080/testes/ServletTestes
	public byte[] getResponseBodyBytes() throws IOException, APIBaseException {
		byte[] resp = readInputStream(getResponseBodyInputStream());
		int cl = getResponseContentLength();
		if(cl>=0 && resp.length!=cl){
			throw new APIBaseException("Tamanho da resposta inválido. Esperado: "+cl+", Recebido: "+resp.length);
		}
		closeConn();
		return resp;
	}
	
	public InputStream getResponseBodyInputStream() throws IOException {// lembrar de fechar depois
		try{//if(getStatusResponseCode()>=200 && getStatusResponseCode()<300) // REVER !!!!!!
			return conn.getInputStream();
		}catch(IOException e){//else 
			InputStream is = conn.getErrorStream();
			if(is!=null) return is;
			else throw e;
		}
	}
	public String getResponseBodyStr() throws Exception {
		return new String(getResponseBodyBytes(),getProp(CHARSET));
	}
	
	public int getConnectTimeout() {
		return connectTimeout;
	}
	public void setConnectTimeout(int connectTimeoutMillis) {
		this.connectTimeout = connectTimeoutMillis;
	}
	public int getReadTimeout() {
		return readTimeout;
	}
	public void setReadTimeout(int readTimeoutMillis) {
		this.readTimeout = readTimeoutMillis;
	}
	
	public Map<String, List<String>> getResponseHeaders() {
		return responseHeaders;
	}
	public String getResponseHeaderValue(String key){
		if(getResponseHeaders()!=null){
			List<String> v = getResponseHeaders().get(key);
			if(v!=null && v.size()>=0) return v.get(0);
		}
		return null;
	}
	
	public int getStatusResponseCode() {
		return statusResponseCode;
	}
	public PasswordAuthentication getPasswordAuthentication() {
		return passwordAuthentication;
	}
	public void setPasswordAuthentication(PasswordAuthentication passwordAuthentication) {
		this.passwordAuthentication = passwordAuthentication;
	}

	public byte[] getDataPost() {
		return dataPost;
	}

	public void setDataPost(byte[] dataPost) {
		this.dataPost = dataPost;
	}

	
	public String getRequestMethod() {
		if(requestMethod==null){
			if(isSendData()) return "POST";
			else return "GET";
		}
		return requestMethod;
	}

	public void setRequestMethod(String requestMethod) {
		this.requestMethod = requestMethod;
	}

	@Override
	public void close(){
		closeOut();
		closeConn();
	}
	
	@Override
	protected void finalize() throws Throwable {
		close();
	}
	
}





/*
 * 
 * 
 * http://blog.csdn.net/c30gcrk/archive/2007/10/22/1836637.aspx
 * 
 * 
 * testar uma url sem proxy, 
 * se não conseguir tentar detectar o proxy para conectar, 
 * se não conseguir abrir popup com configurações de proxy: host, port, user, pass (o host e port podem ser os detectados)
 * 
 * para detectar basta ler o header da resposta do servidor
 * 
 * VER PROGRESS !!!
 * 
 * opcção de envio normal ou com thread ... (neste caso a thread deve ser de quem chama, aqui só deve ter status... e progress...)
 * 
 * ver envio post, get e multpart ...
 * 
 */

/*
private String getProxyForJRE15AndFurther() throws Exception {
	String proxy = null;
	URI uri = new URI(url.toExternalForm());
    ProxySelector proxySelector = ProxySelector.getDefault();
    List<Proxy> list = proxySelector.select(uri);//(List)ob;
    for (Proxy p : list) {
        InetSocketAddress sAddress = (InetSocketAddress)p.address();
        if (sAddress != null) {
        	proxy = sAddress.getHostName() + ":" + sAddress.getPort();
        	Conf.LOG.config("NetUtils.getProxyForJRE15AndFurther(): ProxySelector returned: " + proxy + ".");            	
        }
    }           
	return proxy;
}*/